主题
Agent 会直接改用户的代码,所以这个模块要回答的第一个问题是——改坏了怎么办,而且退回的动作不能污染用户自己项目的 Git 历史。我的方案叫 Side-Git:在项目外面建一个隐藏的快照仓库,每个任务回合拍快照,出问题就整回合回滚——全程用户自己的 .git 零接触。
90 秒口述版
“Agent 敢放心改代码的前提,不是它永远不犯错,而是改坏了能退回——而且退回不能污染用户自己项目的 Git 历史。所以我做了一个独立的快照仓库:用纯 Java 的 Git 库(JGit),在用户目录下按路径哈希建一个隐藏仓库,让它的工作目录指向用户项目根,但所有提交都写进外面这个隐藏仓库。这样加文件、存版本,扫的都是用户的代码,写的全是我的仓库——用户自己的 .git、分支、stash 全程零接触。
每个任务回合我拍两张快照:任务开始前同步拍,任务结束后异步拍——不拖慢回复。用户点回滚时,我不做任何 checkout 或 reset,而是从快照仓库把目标版本的文件清单读出来,和当前工作区做差异比对:多出来的删掉、变了的写回——全程是纯文件操作。恢复之前还会先给当前状态拍一张『恢复前快照』,保证回滚这个动作本身也能撤销。”
机制四条(口语版)
1. 独立仓库,按路径哈希隔离。 快照仓库放在用户目录下,路径用「父目录哈希 + 项目根哈希」两段标识——不同位置的同名项目不打架,项目搬家也不会错绑旧历史。
2. 隔离的核心是“工作目录指向、存储指向分离”。 扫文件扫的是项目根,存版本存的是外部仓库——用户 Git 的分支、暂存区、历史一个都不碰。这是整个设计的第一红线。
3. 快照时机:开始前同步、结束后异步。 开始前的快照决定回滚基准,必须等扫完才允许 Agent 动手;结束后的快照只做记录,晚几秒落盘无所谓——所以一个同步一个异步,代价是写快照走单线程排队,保证不会两个快照同时写。
4. 回滚 = 纯文件写回。 定位到目标快照 → 先拍一张“恢复前快照”留退路 → 对比目标版本和当前工作区:多出来的文件删掉、变化的文件写回 → 空目录顺手清理。不做 merge、不做 checkout、不做 reset。
追问
为什么不用 git stash,或者在项目里开个分支?
stash 是栈语义,恢复时直接动用户的工作区,还和用户自己的 stash 共用同一个栈——可能把用户暂存的东西顶掉。项目内开分支要动 HEAD 和引用,“不碰用户 Git 历史”是红线。Side-Git 把仓库放到项目外的哈希目录,用户的 .git、reflog、stash 全程零接触。
怎么保证真的碰不到项目的 .git?
三层防线:第一层,Git 库配置成“工作目录指向项目根、存储目录指向外部仓库”,代码里根本没有打开项目 .git 的路径;第二层,.git 本身在排除列表里,连扫描都不会扫到;第三层,回滚写文件时每一个都做项目根前缀校验,防止越界写。
为什么恢复前还要再拍一次快照?
留退路。恢复本身就是批量改写工作区——如果恢复错了(比如选错了版本),没有这一张“恢复前快照”就回不去“恢复之前”了。危险操作自带后悔药,和危险命令走人工审批是同一套防御思路。
为什么只允许回到“任务开始前”,不能回到“任务结束后”?
回滚的语义是“回到任务动手前的干净状态”;“任务结束”的状态可能已经包含改坏的产物,恢复到那里等于从一个坑跳进另一个坑。所以结束后的快照只用来做对比和审计,不作为回滚目标。
为什么恢复用纯文件写回,不走 git checkout?
因为目标不是“操作一个 Git 仓库”,而是“把工作区恢复成某个历史状态、且绝不惊动用户的版本控制”。checkout/reset 动的是用户的版本历史,属于越权。所以我只把 Git 当内容寻址的存储用——读出目标版本的文件清单,diff 出差异,直接删/写文件,把回滚语义收窄到纯文件层面,安全才可控。
排除规则是干什么的?
快照没必要收录“体积大、可再生成、不该入历史”的东西:编译产物、依赖目录、IDE 配置。排除规则写在快照仓库自己的排除文件里,扫描时自动跳过,支持环境变量覆盖,快照数量也有上限防膨胀。
实现备忘(对照源码用,面试不必背)
- 快照入口:SnapshotService.runTurn(模板方法——pre 同步 / post 单线程 executor 异步 / pre-restore)
- 隔离实现:SideGitManager(纯 JGit);FileRepositoryBuilder.setGitDir(外部仓库) + setWorkTree(项目根)
- 快照仓库路径:~/.codeagent/snapshots/<父目录哈希>/<项目根哈希>/,SHA-256 前 8 字节两段哈希
- 排除规则:写 side 仓库 info/exclude,默认排除 .git/target/node_modules/dist/IDE 配置/编译产物,CODEAGENT_SNAPSHOT_* 可覆盖(开关/上限 50/排除项)
- 恢复:restorePreTurn(offset) → pre-restore 快照 → TreeWalk 读目标树 → 删多余文件(prune 空目录)→ ObjectLoader 逐文件写回;listPreTurnSnapshots 只筛 PRE_TURN 相位